Вы решили открыть небольшое кафе в Москве. Оно оригинальное — гостей должны обслуживать роботы. Проект многообещающий, но дорогой. Вместе с партнёрами вы решились обратиться к инвесторам. Их интересует текущее положение дел на рынке — сможете ли вы снискать популярность на долгое время, когда все зеваки насмотрятся на роботов-официантов?
Вы — гуру аналитики, и партнёры просят вас подготовить исследование рынка. У вас есть открытые данные о заведениях общественного питания в Москве.
Загрузите данные о заведениях общественного питания Москвы. Убедитесь, что тип данных в каждой колонке — правильный, а также отсутствуют пропущенные значения и дубликаты. При необходимости обработайте их.
Путь к файлу: /datasets/rest_data.csv
Сделайте общий вывод и дайте рекомендации о виде заведения, количестве посадочных мест, а также районе расположения. Прокомментируйте возможность развития сети.
Подготовьте презентацию исследования для инвесторов. Для создания презентации используйте любой удобный инструмент, но отправить презентацию нужно обязательно в формате pdf. Приложите ссылку на презентацию в markdown-ячейке в формате:
Презентация: <ссылка на облачное хранилище с презентацией>
Следуйте принципам оформления из темы «Подготовка презентации».
Таблица rest_data:
import pandas as pd
import re
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as go
pd.set_option('display.max_colwidth', None)
pd.options.display.float_format = '{:,.0f}'.format
try:
df = pd.read_csv("/datasets/rest_data.csv")
except:
print('Ошибка подгрузки данных')
df.info()
df.sample(5)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 15366 entries, 0 to 15365 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 15366 non-null int64 1 object_name 15366 non-null object 2 chain 15366 non-null object 3 object_type 15366 non-null object 4 address 15366 non-null object 5 number 15366 non-null int64 dtypes: int64(2), object(4) memory usage: 720.4+ KB
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 3755 | 27019 | ГОСТИНИЦА ЛЕФОРТОВО | нет | ресторан | город Москва, 1-й Краснокурсантский проезд, дом 1/4 | 200 |
| 13181 | 197545 | Кафе SAJ | нет | кафе | город Москва, Большой Факельный переулок, дом 12 | 28 |
| 1303 | 140412 | Кафе Кальян | нет | кафе | город Москва, улица Покровка, дом 21-23/25, строение 1 | 15 |
| 3986 | 28822 | столовая при школе №450 | нет | столовая | город Москва, 3-я Владимирская улица, дом 30А | 240 |
| 1676 | 25619 | Шоколадница | да | кафе | город Москва, Зелёный проспект, дом 22 | 49 |
df.id.nunique()
15366
Данные корректны
df.object_name.sample(5)
249 КОТИКИЛЮДИ 8739 Сити Пицца 13497 Тануки 8702 Coffeeshop Company 8488 Starbucks Name: object_name, dtype: object
Для дальнейшего поиска и удаления дубликатов приведем названия к нижнему регистру
df.object_name = df.object_name.str.lower()
df.chain.value_counts()
нет 12398 да 2968 Name: chain, dtype: int64
Данные корректны
df.object_type.value_counts()
кафе 6099 столовая 2587 ресторан 2285 предприятие быстрого обслуживания 1923 бар 856 буфет 585 кафетерий 398 закусочная 360 магазин (отдел кулинарии) 273 Name: object_type, dtype: int64
Заменим тип "предприятие быстрого обслуживания" и "магазин (отдел кулинарии)" на более короткие - для удобного отображения на графиках, в остальном данные корректны
df.loc[(df.object_type == "предприятие быстрого обслуживания"), 'object_type'] = 'фаст-фуд'
df.loc[(df.object_type == "магазин (отдел кулинарии)"), 'object_type'] = 'кулинария'
df.address.sample(5)
6309 город Москва, Павелецкая площадь, дом 2, строение 1 5797 город Москва, Рязанский проспект, дом 2, корпус 2 1857 город Москва, поселение Новофедоровское, деревня Рассудово, улица Рассудовское Лесничество, дом 9А 9979 город Москва, Первомайская улица, дом 42 4435 город Москва, улица Екатерины Будановой, дом 18 Name: address, dtype: object
Для дальнейшего поиска и удаления дубликатов приведем названия к нижнему регистру
df.address = df.address.str.lower()
df.number.value_counts()
0 1621
40 835
20 727
30 685
10 644
...
172 1
520 1
680 1
760 1
495 1
Name: number, Length: 315, dtype: int64
Много заведений с нулем посадочных мест, посмотрим на них
df.query('number==0').sample(15)
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 9949 | 171089 | точка продажи готовой еды милти | да | фаст-фуд | город москва, кировоградская улица, дом 23а, корпус 1 | 0 |
| 13744 | 218420 | шаурма | нет | фаст-фуд | город москва, покровская улица, дом 23 | 0 |
| 15197 | 213381 | теремок | да | фаст-фуд | город москва, варшавское шоссе, дом 87б | 0 |
| 11518 | 183279 | суши стор | нет | фаст-фуд | город москва, ореховый бульвар, дом 14, корпус 3, строение 4 | 0 |
| 9928 | 170941 | шаурма | нет | фаст-фуд | город москва, улица народного ополчения, дом 22, корпус 1 | 0 |
| 8490 | 148617 | ploveberry | нет | кафе | город москва, поселение сосенское, калужское шоссе, 22-й километр, дом 10 | 0 |
| 11094 | 177952 | донеретт пекарня | нет | закусочная | город москва, улица декабристов, дом 43, строение 1 | 0 |
| 1873 | 79718 | макдоналдс | да | ресторан | город москва, поселение московский, деревня говорово, дом 1б/н | 0 |
| 12373 | 199714 | кофе | нет | фаст-фуд | город москва, улица правды, дом 7/9 | 0 |
| 14913 | 222814 | wowfest | нет | кафе | город москва, ходынский бульвар, дом 4 | 0 |
| 10133 | 172624 | пекарня | нет | фаст-фуд | город москва, алтуфьевское шоссе, дом 70, корпус 1 | 0 |
| 12003 | 174844 | кофе с собой | да | кафетерий | город москва, люсиновская улица, дом 36/50 | 0 |
| 9028 | 161377 | пивная | нет | фаст-фуд | город москва, туристская улица, дом 31, корпус 1 | 0 |
| 13737 | 223333 | шаурмастер | нет | фаст-фуд | город москва, площадь тверская застава, дом 3 | 0 |
| 15314 | 211310 | милти | да | кулинария | город москва, бульвар маршала рокоссовского, дом 31 | 0 |
Закусочные, кулинарии, шаурма могут не иметь посадочных мест; данные корректны
df.duplicated(subset=['object_name', 'chain', 'object_type', 'address', 'number']).sum()
85
df.drop_duplicates(subset=['object_name', 'address', 'chain', 'object_type', 'number'], inplace = True)
Привели текстовые значения к нижнему регистру, проверили данные на корректность, пропуски и дубликаты; данные готовы к дальнейшему исследованию
ratio_amount = df.object_type.value_counts().reset_index()
ratio_amount.columns = ['type', 'amount']
ratio_amount
| type | amount | |
|---|---|---|
| 0 | кафе | 6071 |
| 1 | столовая | 2584 |
| 2 | ресторан | 2282 |
| 3 | фаст-фуд | 1897 |
| 4 | бар | 855 |
| 5 | буфет | 576 |
| 6 | кафетерий | 395 |
| 7 | закусочная | 348 |
| 8 | кулинария | 273 |
plt.figure(figsize=(15, 7))
ax = sns.barplot(x='type', y='amount', data=ratio_amount)
for index, row in ratio_amount.iterrows():
ax.text(row.name, row.amount + 50, row.amount, color='black', ha="center", fontsize = 13)
plt.title("Количество объектов общественного питания по видам", fontsize = 15)
plt.xlabel("Вид объекта питания", fontsize = 13)
plt.ylabel("Количество объектов", fontsize = 13);
Ожидаемо самый распространненый вид объекта - это кафе (6071), столовых (2584) и ресторанов (2282) более чем в два раза меньше; меньше всего кулинарий (273) и закусочных (348)
chain_amount = df.chain.value_counts().reset_index()
chain_amount.columns = ['chain', 'amount']
chain_amount
| chain | amount | |
|---|---|---|
| 0 | нет | 12317 |
| 1 | да | 2964 |
fig = go.Figure(data=[go.Pie(labels=['несетевые', 'сетевые'], values=chain_amount.amount)])
fig.update_layout(
title={'text':"Количество сетевых и несетевых заведений", 'x':0.71, 'y':0.9},
legend=dict(x=.8, y=.9)
)
fig.show()
В исследовании 12317 (81%) сетевых и 2964 (19%) несетевых заведений
chain_count = df.query('chain == "да"').object_type.value_counts().reset_index()
chain_count.columns = ['type', 'chain_amount']
chain_count
| type | chain_amount | |
|---|---|---|
| 0 | кафе | 1396 |
| 1 | фаст-фуд | 788 |
| 2 | ресторан | 543 |
| 3 | кулинария | 78 |
| 4 | закусочная | 56 |
| 5 | кафетерий | 52 |
| 6 | бар | 37 |
| 7 | буфет | 11 |
| 8 | столовая | 3 |
Объеденим chain_count с таблицей ratio_amount по столбцу type и посчитаем долю сетевых заведений от общего числа
chain_share = ratio_amount.merge(chain_count, on='type')
chain_share['chain_ratio'] = chain_share.chain_amount / chain_share.amount
chain_share.style.format({'chain_ratio': '{:.1%}'})
| type | amount | chain_amount | chain_ratio | |
|---|---|---|---|---|
| 0 | кафе | 6071 | 1396 | 23.0% |
| 1 | столовая | 2584 | 3 | 0.1% |
| 2 | ресторан | 2282 | 543 | 23.8% |
| 3 | фаст-фуд | 1897 | 788 | 41.5% |
| 4 | бар | 855 | 37 | 4.3% |
| 5 | буфет | 576 | 11 | 1.9% |
| 6 | кафетерий | 395 | 52 | 13.2% |
| 7 | закусочная | 348 | 56 | 16.1% |
| 8 | кулинария | 273 | 78 | 28.6% |
chain_share_sort = chain_share.sort_values(by='chain_ratio', ascending=False)
plt.figure(figsize=(17, 7))
ax = sns.barplot(x='type', y='chain_ratio', data=chain_share_sort)
for i, val in enumerate(chain_share_sort.chain_ratio.values):
plt.text(i, val, round(val*100,2), horizontalalignment='center', verticalalignment='bottom',fontsize=13)
plt.title("Размер доли сетей для каждого вида общественного питания", fontsize = 15)
plt.xlabel("Вид общественного питания", fontsize = 13)
plt.ylabel("Доля сетевых заведений", fontsize = 13)
plt.xticks(rotation=45);
Самая высокая доля сетевых заведений в фаст-фуде (42%), в кафе и ресторанах доля 23% и 24% соотвественно; сетевых столовых, буфетов или баров почти нет
Для ответа на поставленный вопрос нам надо провести дополнительную обработку данных, для 20-ти самых крупных сетей проведем лемматизацию названий - создадим функцию, которая будет приводить разные названия одних и тех же сетей к одному общему
df.query('chain == "да"').object_name.value_counts().head(20)
шоколадница 157 kfc 155 макдоналдс 150 бургер кинг 137 теремок 94 домино'с пицца 90 крошка картошка 90 суши wok 72 милти 72 папа джонс 51 додо пицца 49 кофе с собой 44 чайхона №1 43 якитория 38 хинкальная 38 subway 34 кофе хаус 34 хлеб насущный 32 тануки 32 starbucks 30 Name: object_name, dtype: int64
def name_overall(name):
if 'шоколадница' in name:
return 'шоколадница'
if 'kfc' in name:
return 'kfc'
if 'макдоналдс' in name:
return 'макдоналдс'
if 'бургер кинг' in name:
return 'бургер кинг'
if 'теремок' in name:
return 'теремок'
if "домино'с" in name:
return "домино'с пицца"
if 'крошка картошка' in name:
return 'крошка картошка'
if 'суши wok' in name:
return 'суши wok'
if 'милти' in name:
return 'милти'
if 'папа джонс' in name:
return 'папа джонс'
if 'додо пицца' in name:
return 'додо пицца'
if 'кофе с собой' in name:
return 'кофе с собой'
if 'чайхона №1' in name:
return 'чайхона №1'
if 'якитория' in name:
return 'якитория'
if 'хинкальная' in name:
return 'хинкальная'
if 'кофе хаус' in name:
return 'кофе хаус'
if ('subway' in name) or ('сабвэй' in name) or ('сабвей' in name):
return 'subway'
if 'хлеб насущный' in name:
return 'хлеб насущный'
if 'тануки' in name:
return 'тануки'
if ('starbucks' in name) or ('старбакс' in name):
return 'starbucks'
if ('cofix' in name) or ('кофикс' in name):
return 'cofix'
return name
Применим функцию name_overall и соберем таблицу по названиям сетей с количеством заведений и со средним, минимальным и максимальным числом посадочных мест
df.loc[df.chain == "да",'object_name'] = df.object_name.apply(name_overall)
report = df.query('chain == "да"').groupby('object_name').agg({'object_name': 'count', 'number':['median', 'min', 'max']})
report.columns = ['rest_count', 'seat_median', 'seat_min', 'seat_max']
report.sort_values(by='rest_count', ascending = False).head(20)
| rest_count | seat_median | seat_min | seat_max | |
|---|---|---|---|---|
| object_name | ||||
| kfc | 188 | 49 | 0 | 400 |
| шоколадница | 185 | 50 | 7 | 150 |
| макдоналдс | 172 | 76 | 0 | 580 |
| бургер кинг | 159 | 45 | 0 | 150 |
| теремок | 111 | 24 | 0 | 200 |
| домино'с пицца | 99 | 16 | 4 | 50 |
| крошка картошка | 96 | 15 | 0 | 120 |
| милти | 81 | 0 | 0 | 40 |
| суши wok | 76 | 6 | 0 | 22 |
| starbucks | 69 | 41 | 8 | 120 |
| папа джонс | 67 | 20 | 0 | 63 |
| subway | 60 | 15 | 0 | 42 |
| хинкальная | 54 | 48 | 16 | 150 |
| додо пицца | 54 | 30 | 0 | 70 |
| чайхона №1 | 52 | 132 | 0 | 500 |
| якитория | 50 | 100 | 25 | 456 |
| кофе с собой | 47 | 0 | 0 | 10 |
| тануки | 47 | 98 | 43 | 206 |
| кофе хаус | 40 | 50 | 0 | 150 |
| хлеб насущный | 33 | 40 | 15 | 88 |
Отфильтруем "сети" с числом заведений меньше 3-х и построим диаграмму рассеивания
report.loc[(report.rest_count<3) & (report.seat_median<46), ['category']] = 'Few_rest_Few_seat'
report.loc[(report.rest_count<3) & (report.seat_median>45), ['category']] = 'Few_rest_Many_seat'
report.loc[(report.rest_count>2) & (report.seat_median<46), ['category']] = 'Many_rest_Few_seat'
report.loc[(report.rest_count>2) & (report.seat_median>45), ['category']] = 'Many_rest_Many_seat'
plt.figure(figsize=(10,7))
sns.scatterplot(data=report, x='seat_median', y='rest_count', hue='category')
plt.xlabel('Медианное число посадочных мест', fontsize = 13)
plt.ylabel('Число заведений в сети', fontsize = 13);
seats = df.groupby('object_type').agg({'number':'median'}).sort_values(by='number', ascending=False).reset_index()
seats.columns = ['type', 'seat']
seats
| type | seat | |
|---|---|---|
| 0 | столовая | 103 |
| 1 | ресторан | 80 |
| 2 | бар | 35 |
| 3 | буфет | 32 |
| 4 | кафе | 30 |
| 5 | кафетерий | 6 |
| 6 | фаст-фуд | 6 |
| 7 | закусочная | 0 |
| 8 | кулинария | 0 |
plt.figure(figsize=(15, 7))
ax = sns.barplot(x='type', y='seat', data=seats)
for index, row in seats.iterrows():
ax.text(row.name, row.seat + 1, row.seat, color='black', ha="center", fontsize = 13)
plt.title("Среднее число посадочных мест у различных видов общественного питания", fontsize = 15)
plt.xlabel("Вид объекта питания", fontsize = 13)
plt.ylabel("Число посадочых мест", fontsize = 13);
Самый вместительный вид - столовая (103 посадочных места), что логично, т.к. столовые обслуживают места, где изначально предполагается большое количество посетителей, далее идут рестораны (80 мест); закусочные и кулинарии работаю в основном "на вынос"
#df['streetname'] = df.address.transform(lambda x: str(x).split(", ")[1]).str.lower().str.strip()
name_street = ['улица',
'бульвар',
'шоссе',
'переулок',
'проезд',
'проспект',
'набережная',
'линия',
'тупик',
'километр',
'аллея',
'площадь',
'капотня',
'микрорайон',
'просек',
'территория'
]
name_suburb = ['город зеленоград',
'город',
'деревня',
'посёлок',
'поселение'
]
def insert_street(data):
address = re.split(", |,", data['address'])
for i in name_street:
if i in address[0]:
return address[0]
if i in address[1]:
return address[1]
for i in name_suburb:
if i in address[1]:
return address[1] + ', '+address[2]
df['streetname'] = df.apply(insert_street, axis=1)
top_10 = df.groupby('streetname').agg({'id':'count'}).sort_values(by='id', ascending=False).reset_index()
top_10.columns = ['street', 'amount']
top_10_f = top_10.head(10)
top_10_f
| street | amount | |
|---|---|---|
| 0 | проспект мира | 203 |
| 1 | профсоюзная улица | 182 |
| 2 | ленинградский проспект | 172 |
| 3 | пресненская набережная | 167 |
| 4 | варшавское шоссе | 162 |
| 5 | ленинский проспект | 148 |
| 6 | проспект вернадского | 128 |
| 7 | кутузовский проспект | 114 |
| 8 | каширское шоссе | 111 |
| 9 | кировоградская улица | 108 |
plt.figure(figsize=(17, 7))
ax = sns.barplot(x='street', y='amount', data=top_10_f)
for i, val in enumerate(top_10_f.amount.values):
plt.text(i, val, val, horizontalalignment='center', verticalalignment='bottom',fontsize=13)
plt.title("Топ-10 улиц по количеству объектов общественного питания Москвы", fontsize = 15)
plt.xlabel("Название улицы", fontsize = 13)
plt.ylabel("Число заведений", fontsize = 13)
plt.xticks(rotation=45);
Используем внешний источник и также переведем названия улиц к строчному написанию
area_msc = pd.read_csv('https://frs.noosphere.ru/xmlui/bitstream/handle/20.500.11925/714058/mosgaz-streets.csv?sequence=1&isAllowed=y')
area_msc['streetname'] = area_msc.streetname.str.lower()
area_msc.sample(5)
| streetname | areaid | okrug | area | |
|---|---|---|---|---|
| 2905 | 2-й красногорский проезд | 93 | СЗАО | Район Щукино |
| 651 | колымажный переулок | 12 | ЦАО | Район Арбат |
| 3916 | профсоюзная улица | 134 | ЮЗАО | Район Ясенево |
| 2521 | всеволожский переулок | 20 | ЦАО | Район Хамовники |
| 4350 | последний переулок | 16 | ЦАО | Мещанский район |
Удалим ненужные столбцы, переименуем streetname в street и объединим таблицы top_10_f и area_msc
area_msc.drop(['areaid', 'okrug'], axis='columns', inplace=True)
area_msc = area_msc.rename(columns={'streetname': 'street'})
top_10_area = pd.merge(top_10_f , area_msc, on='street', how='left')
top_10_area_group = pd.pivot_table(top_10_area, index=['street','area']).sort_values(by='amount', ascending=False)
top_10_area_group.drop(['amount'], axis='columns', inplace=True)
top_10_area_group
| street | area |
|---|---|
| проспект мира | Район Ростокино |
| Ярославский Район | |
| Алексеевский район | |
| Мещанский район | |
| Останкинский район | |
| Район Марьина роща | |
| Район Свиблово | |
| профсоюзная улица | Район Ясенево |
| Академический район | |
| Обручевский район | |
| Район Коньково | |
| Район Теплый Стан | |
| Район Черемушки | |
| ленинградский проспект | Район Сокол |
| Хорошевский район | |
| Район Аэропорт | |
| Район Беговой | |
| пресненская набережная | Пресненский район |
| варшавское шоссе | Нагорный район |
| Донской район | |
| Район Нагатино-Садовники | |
| Район Северное Бутово | |
| Район Чертаново Северное | |
| Район Чертаново Центральное | |
| Район Чертаново Южное | |
| Район Южное Бутово | |
| ленинский проспект | Район Гагаринский |
| Донской район | |
| Ломоносовский район | |
| Обручевский район | |
| Район Проспект Вернадского | |
| Район Теплый Стан | |
| Район Тропарево-Никулино | |
| Район Якиманка | |
| проспект вернадского | Район Тропарево-Никулино |
| Район Раменки | |
| Район Проспект Вернадского | |
| Район Гагаринский | |
| Ломоносовский район | |
| кутузовский проспект | Район Дорогомилово |
| Район Фили-Давыдково | |
| каширское шоссе | Район Орехово-Борисово Южное |
| Район Орехово-Борисово Северное | |
| Район Нагатино-Садовники | |
| Район Москворечье-Сабурово | |
| кировоградская улица | Район Чертаново Центральное |
| Район Чертаново Северное | |
| Район Чертаново Южное |
Данные улицы популярны для заведений, потому большая часть из них это - "лучи" Москвы, очень длинные улицы тянущиеся от центра города до спальных районов; по ним всегда перемещается много людей, у них отличная транспортная доступность; на этих улицах много помещений пригодных для общепита
Отфильтруем улицы с одним заведением питания
one_cf = top_10.query('amount==1')
Объединим полученную таблицу с таблицей районов, удалим строки пустыми значениями
one_cf_area = pd.merge(one_cf, area_msc, how='left', on='street')
one_cf_area.dropna(subset=['area'], inplace=True)
one_cf_area.drop(['amount'], axis='columns', inplace=True)
one_cf_area
| street | area | |
|---|---|---|
| 0 | архангельский переулок | Басманный район |
| 1 | большая юшуньская улица | Район Зюзино |
| 2 | большая ширяевская улица | Район Сокольники |
| 3 | большой афанасьевский переулок | Район Арбат |
| 4 | большой афанасьевский переулок | Район Хамовники |
| ... | ... | ... |
| 737 | дмитровский переулок | Тверской район |
| 738 | дивизионная улица | Район Внуково |
| 739 | улица 9 мая | Район Восточный |
| 740 | улица авиаконструктора микояна | Хорошевский район |
| 745 | яхромская улица | Дмитровский район |
563 rows × 2 columns
Число улиц - 563
Определим топ-10 райнов Москвы, где больше всего улиц с одним заведением питания
one_cf_area_group = one_cf_area.groupby('area').agg({'street':'count'}).sort_values(by='street', ascending=False).reset_index().head(10)
one_cf_area_group
| area | street | |
|---|---|---|
| 0 | Таганский район | 27 |
| 1 | Район Хамовники | 26 |
| 2 | Басманный район | 25 |
| 3 | Пресненский район | 20 |
| 4 | Тверской район | 20 |
| 5 | Район Арбат | 18 |
| 6 | Район Марьина роща | 18 |
| 7 | Мещанский район | 15 |
| 8 | Район Сокольники | 15 |
| 9 | Район Замоскворечье | 14 |
plt.figure(figsize=(17, 7))
ax = sns.barplot(x='area', y='street', data=one_cf_area_group)
for i, val in enumerate(one_cf_area_group.street.values):
plt.text(i, val, val, horizontalalignment='center', verticalalignment='bottom',fontsize=13)
plt.title("Районы Москвы, где больше всего улиц с одним заведением общепита", fontsize = 15)
plt.xlabel("Название района", fontsize = 13)
plt.ylabel("Число улиц", fontsize = 13)
plt.xticks(rotation=45);
Всего в Москве 563 улицы с одним заведением общественного питания; это, как правило, "периферийные" небольшие улицы, переулки или тупики, и даже если они расположены в центре им может не хватать "трафика" для открытия еще одного заведения или на этих улицах может просто не быть свободных подходящих помещений
Презентация: https://disk.yandex.ru/i/HTwjlPjESyktcg